Skip to content

Conversation

cabljac
Copy link

@cabljac cabljac commented Sep 30, 2025

This draft PR is a proposed solution to resolve #1712

Problem

When an analytics event payload contains malformed user properties, the SDK throws a TypeError before the user's callback can execute:

// Problematic payloads:
userProperties: {
  "some_property": null,                    // Entirely null
  "another_property": { "value": null },    // Value field is null
  "empty_property": {}                      // Missing value field
}

Error:

TypeError: Cannot convert undefined or null to object
at Function.keys ()
at unwrapValueAsString (analytics.js:206)

Solution

  1. Made unwrapValueAsString defensive
  • Added null/undefined/empty object checks before calling Object.keys()
  • Returns empty string for invalid inputs instead of throwing
  • Improved type safety by changing parameter from any to unknown
  1. Filter invalid user properties
  • Added filter in UserDimensions constructor to skip properties that are:
    • null or undefined
    • Empty objects {}
    • Objects with value field that is null, undefined, or {}
  • Valid properties (including falsy values like "" and 0) are preserved

@taeold
Copy link
Contributor

taeold commented Oct 6, 2025

/gemini review

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request provides a solid fix for a crash caused by malformed user properties in analytics events. The approach is robust, adding defensive checks in unwrapValueAsString and filtering invalid properties during event construction. The addition of a new test case that covers various invalid inputs is excellent for ensuring correctness and preventing future regressions. I have one suggestion to improve the readability of the new filtering logic.

Comment on lines +184 to +208
.filter(([, v]) => {
if (v === null || v === undefined) {
return false;
}
if (typeof v !== "object") {
return false;
}
const vObj = v as Record<string, unknown>;
// Filter out empty objects
if (Object.keys(vObj).length === 0) {
return false;
}
// Filter if 'value' property exists but is null/undefined/empty object
if ("value" in vObj) {
const value = vObj.value;
if (
value === null ||
value === undefined ||
(typeof value === "object" && value !== null && Object.keys(value).length === 0)
) {
return false;
}
}
return true;
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The filtering logic here is comprehensive but also quite complex and spread across multiple conditional statements, which could affect readability and future maintenance. To make the logic clearer and more self-documenting, you could refactor it slightly by grouping related checks and adding comments to explain each validation step.

        .filter(([, v]) => {
          // Property must be a non-empty object.
          if (v == null || typeof v !== "object") {
            return false;
          }
          if (Object.keys(v).length === 0) {
            return false;
          }

          // If 'value' field exists, it must not be null, undefined, or an empty object.
          if ("value" in v) {
            const value = (v as { value: unknown }).value;
            if (value == null || (typeof value === "object" && value !== null && Object.keys(value).length === 0)) {
              return false;
            }
          }

          return true;
        })

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Intermittent TypeError for app_remove events
2 participants